/*
 * JBoss, Home of Professional Open Source
 * Copyright 2006, Red Hat, Inc. and individual contributors
 * by the @authors tag. See the copyright.txt in the distribution for a
 * full listing of individual contributors.
 *
 * This is free software; you can redistribute it and/or modify it
 * under the terms of the GNU Lesser General Public License as
 * published by the Free Software Foundation; either version 2.1 of
 * the License, or (at your option) any later version.
 *
 * This software is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
 * Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public
 * License along with this software; if not, write to the Free
 * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA
 * 02110-1301 USA, or see the FSF site: http://www.fsf.org.
 */

package org.jdiameter.server.impl.app.auth;

import static org.jdiameter.common.api.app.auth.ServerAuthSessionState.DISCONNECTED;
import static org.jdiameter.common.api.app.auth.ServerAuthSessionState.IDLE;
import static org.jdiameter.common.api.app.auth.ServerAuthSessionState.OPEN;
import static org.jdiameter.server.impl.app.auth.Event.Type.RECEIVE_ASR_ANSWER;
import static org.jdiameter.server.impl.app.auth.Event.Type.RECEIVE_AUTH_REQUEST;
import static org.jdiameter.server.impl.app.auth.Event.Type.TIMEOUT_EXPIRES;

import java.io.Serializable;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

import org.jdiameter.api.Answer;
import org.jdiameter.api.EventListener;
import org.jdiameter.api.IllegalDiameterStateException;
import org.jdiameter.api.InternalException;
import org.jdiameter.api.NetworkReqListener;
import org.jdiameter.api.OverloadException;
import org.jdiameter.api.Request;
import org.jdiameter.api.RouteException;
import org.jdiameter.api.app.AppAnswerEvent;
import org.jdiameter.api.app.AppEvent;
import org.jdiameter.api.app.AppRequestEvent;
import org.jdiameter.api.app.AppSession;
import org.jdiameter.api.app.StateChangeListener;
import org.jdiameter.api.app.StateEvent;
import org.jdiameter.api.auth.ServerAuthSession;
import org.jdiameter.api.auth.ServerAuthSessionListener;
import org.jdiameter.api.auth.events.AbortSessionAnswer;
import org.jdiameter.api.auth.events.AbortSessionRequest;
import org.jdiameter.api.auth.events.ReAuthRequest;
import org.jdiameter.api.auth.events.SessionTermAnswer;
import org.jdiameter.api.auth.events.SessionTermRequest;
import org.jdiameter.client.api.ISessionFactory;
import org.jdiameter.common.api.app.IAppSessionState;
import org.jdiameter.common.api.app.auth.IAuthMessageFactory;
import org.jdiameter.common.api.app.auth.IServerAuthActionContext;
import org.jdiameter.common.api.app.auth.ServerAuthSessionState;
import org.jdiameter.common.impl.app.AppAnswerEventImpl;
import org.jdiameter.common.impl.app.auth.AbortSessionAnswerImpl;
import org.jdiameter.common.impl.app.auth.AbortSessionRequestImpl;
import org.jdiameter.common.impl.app.auth.AppAuthSessionImpl;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
 * Server Authorization session implementation
 * 
 * @author erick.svenson@yahoo.com
 * @author <a href="mailto:brainslog@gmail.com"> Alexandre Mendonca </a>
 * @author <a href="mailto:baranowb@gmail.com"> Bartosz Baranowski </a>
 */
public class ServerAuthSessionImpl extends AppAuthSessionImpl implements ServerAuthSession, EventListener<Request, Answer>, NetworkReqListener {

  private static final long serialVersionUID = 1L;

  protected static final Logger logger = LoggerFactory.getLogger(ServerAuthSessionImpl.class);

  protected IServerAuthSessionData sessionData;

  // Session State Handling ---------------------------------------------------
  private Lock sendAndStateLock = new ReentrantLock();

  // Factories and Listeners --------------------------------------------------
  protected transient IAuthMessageFactory factory;
  protected transient IServerAuthActionContext context;
  protected transient ServerAuthSessionListener listener;

  // Ts Timer -----------------------------------------------------------------
  protected final static String TIMER_NAME_TS="AUTH_TS";

  // Constructors -------------------------------------------------------------

  public ServerAuthSessionImpl(IServerAuthSessionData sessionData,ISessionFactory sf, ServerAuthSessionListener lst, IAuthMessageFactory fct, StateChangeListener<AppSession> scListener,IServerAuthActionContext context, long tsTimeout, boolean stateless) {
    super(sf,sessionData);  

    super.appId = fct.getApplicationId();
    this.listener = lst;
    this.factory = fct;
    this.context = context;
    this.sessionData = sessionData;
    this.sessionData.setStateless(stateless);
    this.sessionData.setTsTimeout(tsTimeout);
    super.addStateChangeNotification(scListener);
  }

  // ServerAuthSession Implementation methods ---------------------------------

  public void sendAuthAnswer(AppAnswerEvent appAnswerEvent) throws InternalException, IllegalDiameterStateException, RouteException, OverloadException {
    send(null, appAnswerEvent);
  }

  public void sendReAuthRequest(ReAuthRequest reAuthRequest) throws InternalException, IllegalDiameterStateException, RouteException, OverloadException {
    send(null, reAuthRequest);
  }

  public void sendAbortSessionRequest(AbortSessionRequest abortSessionRequest) throws InternalException, IllegalDiameterStateException, RouteException, OverloadException {
    send(Event.Type.SEND_ASR_REQUEST, abortSessionRequest);
  }

  public void sendSessionTerminationAnswer(SessionTermAnswer sessionTermAnswer) throws InternalException, IllegalDiameterStateException, RouteException, OverloadException {
    send(null, sessionTermAnswer);
  }

  protected void send(Event.Type type, AppEvent event) throws InternalException {
    try {
      sendAndStateLock.lock();
      if (type != null) {
        handleEvent(new Event(type, event));
      }
      session.send(event.getMessage(), this);
    }
    catch (Exception e) {
      throw new InternalException(e);
    }
    finally {
      sendAndStateLock.unlock();
    }
  }

  public boolean isStateless() {
    return sessionData.isStateless();
  }

  @SuppressWarnings("unchecked")
  protected void setState(ServerAuthSessionState newState) {
    IAppSessionState oldState = sessionData.getServerAuthSessionState();
    sessionData.setServerAuthSessionState(newState);
    for (StateChangeListener i : stateListeners) {
      i.stateChanged(this,(Enum) oldState, (Enum) newState);
    }
  }

  @SuppressWarnings("unchecked")
  public <E> E getState(Class<E> eClass) {
    return eClass == ServerAuthSessionState.class ? (E) sessionData.getServerAuthSessionState() : null;
  }

  public boolean handleEvent(StateEvent event) throws InternalException, OverloadException {
    return isStateless() ? handleEventForStatelessSession(event) : handleEventForStatefullSession(event);
  }

  public boolean handleEventForStatelessSession(StateEvent event) throws InternalException, OverloadException {
    try {
      switch (sessionData.getServerAuthSessionState()) {
      case IDLE:
        switch ((Event.Type) event.getType()) {
        case RECEIVE_AUTH_REQUEST:
          // Current State: IDLE
          // Event: Service-specific authorization request received, and successfully processed
          // Action: Send service specific answer
          // New State: IDLE
        	setState(IDLE);
          listener.doAuthRequestEvent(this, (AppRequestEvent) event.getData());
          break;
        default:
          logger.debug("Unknown event {}", event.getType());
          break;
        }
        break;
      }
    }
    catch (Throwable t) {
      throw new InternalException(t);
    }

    return true;
  }

  public boolean handleEventForStatefullSession(StateEvent event) throws InternalException, OverloadException {
    ServerAuthSessionState state = sessionData.getServerAuthSessionState(); 
    ServerAuthSessionState oldState = state;
    try {
      switch (state) {
      case IDLE: {
        switch ((Event.Type) event.getType()) {
        case RECEIVE_AUTH_REQUEST:
          try {
            // Current State: IDLE
            // Event: Service-specific authorization request received, and user is authorized
            // Action: Send successful service specific answer
            // New State: OPEN
        	  setState(OPEN);
            listener.doAuthRequestEvent(this, (AppRequestEvent) event.getData());
          }
          catch (Exception e) {
            setState(IDLE);
          }
          break;
        case RECEIVE_STR_REQUEST:
          try {
            // Current State: ANY
            // Event: STR Received
            // Action: Send STA, Cleanup
            // New State: IDLE
        	  setState(IDLE);
            listener.doSessionTerminationRequestEvent(this, (SessionTermRequest) event.getData());
          }
          catch (Exception e) {
            logger.debug("Can not handle event", e);
          }
          break;
        case SEND_ASR_REQUEST:
          setState(DISCONNECTED);
          break;
        case TIMEOUT_EXPIRES:
          if (context != null) {
            context.accessTimeoutElapses(this);
          }
          setState(IDLE);
          break;
        default:
          logger.debug("Unknown event {}", event.getType());
          break;
        }
        break;
      }
      case OPEN: {
        switch ((Event.Type) event.getType()) {
        case RECEIVE_AUTH_REQUEST:
          try {
            // Current State: OPEN
            // Event: Service-specific authorization request received, and user is authorized
            // Action: Send successful service specific answer
            // New State: OPEN
            listener.doAuthRequestEvent(this, (AppRequestEvent) event.getData());
          }
          catch (Exception e) {
            // Current State: OPEN
            // Event: Service-specific authorization request received, and user is not authorized
            // Action: Send failed service specific answer, Cleanup
            // New State: IDLE
            setState(IDLE);
          }
          break;
        case RECEIVE_STR_REQUEST:
          try {
            setState(IDLE);
            listener.doSessionTerminationRequestEvent(this, (SessionTermRequest) event.getData());
          }
          catch (Exception e) {
            logger.debug("Can not handle event", e);
          }
          break;
        case SEND_ASR_REQUEST:
          // Current State: OPEN
          // Event: Home server wants to terminate the service
          // Action: Send ASR
          // New State: DISCON
          setState(DISCONNECTED);
          break;
        case TIMEOUT_EXPIRES:
          // Current State: OPEN
          // Event: Authorization-Lifetime (and Auth-Grace-Period) expires on home server.
          // Action: Cleanup
          // New State: IDLE

          // Current State: OPEN
          // Event: Session-Timeout expires on home server
          // Action: Cleanup
          // New State: IDLE
          if (context != null) {
            context.accessTimeoutElapses(this);
          }
          setState(IDLE);
          break;
        default:
          logger.debug("Unknown event {}", event.getType());
          break;
        }
        break;
      }
      case DISCONNECTED: {
        switch ((Event.Type) event.getType()) {
        case SEND_ASR_FAILURE:
          // Current State: DISCON
          // Event: Failure to send ASR
          // Action: Wait, Re-send ASR
          // New State: DISCON
          setState(DISCONNECTED);
          break;
        case RECEIVE_ASR_ANSWER:
          // Current State: DISCON
          // Event: ASR successfully sent and ASA Received with Result-Code
          // Action: Cleanup
          // New State: IDLE
          setState(IDLE);
          listener.doAbortSessionAnswerEvent(this, (AbortSessionAnswer) event.getData());
          break;
        default:
          logger.debug("Unknown event {}", event.getType());
          break;
        }
        break;
      }
      default: {
        logger.debug("Unknown state {}", state);
        break;
      }
      }

      // post processing
      if (oldState != state) {
        if (OPEN.equals(state) && context != null) {
          if(context != null) {
            cancelTsTimer();
            startTsTimer();
          }
          //          scheduler.schedule(new Runnable() {
          //            public void run() {
          //              if (context != null) {
          //                try {
          //                  handleEvent(new Event(TIMEOUT_EXPIRES, null));
          //                }
          //                catch (Exception e) {
          //                  logger.debug("Can not handle event", e);
          //                }
          //              }
          //            }
          //          }, context.createAccessTimer(), TimeUnit.MILLISECONDS);
        }
      }
    }
    catch (Throwable t) {
      throw new InternalException(t);
    }

    return true;
  }

  public void receivedSuccessMessage(Request request, Answer answer) {
    AnswerDelivery rd = new AnswerDelivery();
    rd.session = this;
    rd.request = request;
    rd.answer = answer;
    super.scheduler.execute(rd);
  }

  public void timeoutExpired(Request request) {
    try {
      if (request.getCommandCode() == AbortSessionRequestImpl.code) {
        handleEvent(new Event(Event.Type.SEND_ASR_FAILURE, new AbortSessionRequestImpl(request)));
      }
      else {
        logger.debug("Timeout for unknown request {}", request);
      }
    }
    catch (Exception e) {
      logger.debug("Can not handle event", e);
    }
  }

  public Answer processRequest(Request request) {
    RequestDelivery rd = new RequestDelivery();
    rd.session = this;
    rd.request = request;
    super.scheduler.execute(rd);
    return null;
  }

  /* (non-Javadoc)
   * @see org.jdiameter.common.impl.app.AppSessionImpl#isReplicable()
   */
  @Override
  public boolean isReplicable() {
    return true;
  }

  protected void startTsTimer() {
    try {
      sendAndStateLock.lock();
      if (sessionData.getTsTimeout() > 0) {
        sessionData.setTsTimerId(super.timerFacility.schedule(sessionData.getSessionId(), TIMER_NAME_TS, sessionData.getTsTimeout()));
      }
    }
    finally {
      sendAndStateLock.unlock();
    }
  }

  protected void cancelTsTimer() {
    try {
      sendAndStateLock.lock();
      Serializable tsTimerId = sessionData.getTsTimerId();
      if(tsTimerId != null) {
        super.timerFacility.cancel(tsTimerId);
        sessionData.setTsTimerId(null);
      }
    }
    finally {
      sendAndStateLock.unlock();
    }
  }

  /* (non-Javadoc)
   * @see org.jdiameter.common.impl.app.AppSessionImpl#onTimer(java.lang.String)
   */
  @Override
  public void onTimer(String timerName) {
    if(timerName.equals(TIMER_NAME_TS)) {
      try {
        sendAndStateLock.lock();
        sessionData.setTsTimerId(null);
        handleEvent(new Event(TIMEOUT_EXPIRES, null));
      }
      catch (Exception e) {
        logger.debug("Can not handle event", e);
      }
      finally {
        sendAndStateLock.unlock();
      }
    }
  }

  @Override
  public int hashCode() {
    final int prime = 31;
    int result = super.hashCode();
    result = prime * result + ((sessionData == null) ? 0 : sessionData.hashCode());
    return result;
  }

  @Override
  public boolean equals(Object obj) {
    if (this == obj)
      return true;
    if (!super.equals(obj))
      return false;
    if (getClass() != obj.getClass())
      return false;
    ServerAuthSessionImpl other = (ServerAuthSessionImpl) obj;
    if (sessionData == null) {
      if (other.sessionData != null)
        return false;
    }
    else if (!sessionData.equals(other.sessionData))
      return false;
    return true;
  }

  @Override
  public void release() {
    if (isValid()) {
      try {
        sendAndStateLock.lock();
        super.release();
        this.context = null;
        this.listener = null;
      }
      catch (Exception e) {
        logger.debug("Failed to release session", e);
      }
      finally {
        sendAndStateLock.unlock();
      }
    }
    else {
      logger.debug("Trying to release an already invalid session, with Session ID '{}'", getSessionId());
    }
  }

  private class RequestDelivery implements Runnable {
    ServerAuthSession session;
    Request request;

    public void run() {
      if (request != null) {
        if (request.getCommandCode() == factory.getAuthMessageCommandCode()) {
          try {
            sendAndStateLock.lock();
            handleEvent(new Event(RECEIVE_AUTH_REQUEST, factory.createAuthRequest(request)));
          }
          catch (Exception e) {
            logger.debug("Can not handle event", e);
          }
          finally {
            sendAndStateLock.unlock();
          }
        }
        else {
          try {
            listener.doOtherEvent(session, factory.createAuthRequest(request), null);
          }
          catch (Exception e) {
            logger.debug("Can not handle event", e);
          }
        }
      }
    }
  }

  private class AnswerDelivery implements Runnable {
    ServerAuthSession session;
    Answer answer;
    Request request;

    public void run() {
      try {
        sendAndStateLock.lock();
        if (request.getCommandCode() == factory.getAuthMessageCommandCode()) {
          handleEvent(new Event(RECEIVE_AUTH_REQUEST, factory.createAuthRequest(request)));
        }
        else if (request.getCommandCode() == AbortSessionRequestImpl.code) {
          handleEvent(new Event(RECEIVE_ASR_ANSWER, new AbortSessionAnswerImpl(answer)));
        }
        else {
          listener.doOtherEvent(session, factory.createAuthRequest(request), new AppAnswerEventImpl(answer));
        }
      }
      catch (Exception e) {
        logger.debug("Can not handle event", e);
      }
      finally {
        sendAndStateLock.unlock();
      }
    }
  }

}